"""
 Modbus TestKit: Implementation of Modbus protocol simulator using modbus_tk with wxpython
 This program is GUI version of rtuslave_example.py.
 To run this program install the wxpython package.

 (C)2014 - Hill Kim - hillkim7@gmail.com
 
 This is distributed under GNU LGPL license, see license.txt
"""

import  wx
import cStringIO
import base64
import collections
import modbus_tk
import modbus_tk.defines as cst
import modbus_tk.modbus as modbus
import modbus_tk.modbus_rtu as modbus_rtu
import serial
import wxSerialConfigDialog
from modbus_tk import defines

SlaveBlock = collections.namedtuple('SlaveBlock', ('slave_addr', 'block_name', 'block_type', 'start_addr', 'addr_len'))

modbus_server = None
                       
def get_image(data):
    stream = cStringIO.StringIO(data)
    return wx.ImageFromStream(stream)

# Following code was took from image.py of wxWidget demo package.
# GridBG = PyEmbeddedImage(...)
def get_bg_image():
    return wx.BitmapFromImage(get_image(
        base64.b64decode(
        "iVBORw0KGgoAAAANSUhEUgAAAFQAAABUCAYAAAAcaxDBAAAABHNCSVQICAgIfAhkiAAAAX9J"
        "REFUeJzt3FtqgjEUAOGjBi+tgpt3C751b4XaakXRDYRfiJOQwHyPDVgZCByStLPD8esRwqSI"
        "iP1um138/jnF52adXfs9XybX1qtldu3yfy3+zBHW5tmfqphBYQaFGRRmUNjMsYmVIiLSYpFd"
        "vN3vXY1Gpd9zao0eGd3yMIPCDAozKMygMMcmWIqIKiNOjROs0rXtxya7dvo745/plocZFGZQ"
        "mEFhBoU5NsGqjU2jrNEnWG55mEFhBoUZFGZQmGMTrNrYVONCbYT3Um55mEFhBoUZFGZQmGMT"
        "LEVElUusUS7+fNvUOYPCDAozKMygMMcmWHeXdCOcKE19T7c8zKAwg8IMCjMozLEJ9nJs6ulp"
        "d08Xhp42NWJQmEFhBoUZFObYBBvqbVPrv6QrGRnd8jCDwgwKMyjMoDDHJthQ/7dphDHNLQ8z"
        "KMygMIPCDApzbIK9ddrU8hSnt4s/x6ZGDAozKMygMIPCHJtg3b1tan2CRf8+tzzMoDCDwgwK"
        "MyjsCUFtsOQdiq21AAAAAElFTkSuQmCC")))

#----------------------------------------------------------------------
ID_New = wx.NewId()
ID_PortSettings = wx.NewId()
ID_Exit = wx.NewId()
#----------------------------------------------------------------------

class ModbusConfigDialog(wx.Dialog):
    slave_addr = 0x11
    block_name = "test"
    block_type = "04"
    start_addr = 1000
    addr_len = 10
        
    def __init__(self, *args, **kwds):
        wx.Dialog.__init__(self, *args, **kwds)
        self.label_1 = wx.StaticText(self, -1, "Slave Address")
        self.text_ctrl_slave_addr = wx.TextCtrl(self, -1, "")
        self.label_2 = wx.StaticText(self, -1, "Name")
        self.text_ctrl_block_name = wx.TextCtrl(self, -1, "")
        self.label_3 = wx.StaticText(self, -1, "Block Type")
        self.combo_ctrl_block_type = wx.ComboBox(self, -1, style=wx.CB_READONLY)
        self.combo_ctrl_block_type.Append("%d : Analog Input Registers(30001-39999)" % defines.ANALOG_INPUTS, defines.ANALOG_INPUTS)
        self.combo_ctrl_block_type.Append("%d : Analog Output Holding Registers(40001-49999)" % defines.HOLDING_REGISTERS, defines.HOLDING_REGISTERS)
        self.label_4 = wx.StaticText(self, -1, "Start Address Offset")
        self.text_ctrl_start_addr = wx.TextCtrl(self, -1, "")
        self.label_5 = wx.StaticText(self, -1, "Address Length")
        self.text_ctrl_addr_len = wx.TextCtrl(self, -1, "")

        self.button_ok = wx.Button(self, -1, "OK")
        self.button_cancel = wx.Button(self, -1, "Cancel")

        self.__set_properties()
        self.__do_layout()
 
        #attach the event handlers
        self.__attach_events()

    def __set_properties(self):
        self.SetTitle("Add MODBUS slave block")
        self.text_ctrl_slave_addr.SetValue(str(self.slave_addr))
        self.text_ctrl_block_name.SetValue(self.block_name)
        self.combo_ctrl_block_type.Select(0)
        self.text_ctrl_start_addr.SetValue(str(self.start_addr))
        self.text_ctrl_addr_len.SetValue(str(self.addr_len))
        self.button_ok.SetDefault()

    def __do_layout(self):
        sizer_2 = wx.BoxSizer(wx.VERTICAL)

        sizer_slave = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Slave"), wx.VERTICAL)
        sizer_x = wx.BoxSizer(wx.HORIZONTAL)
        sizer_x.Add(self.label_1, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
        sizer_x.Add(self.text_ctrl_slave_addr, 1, 0, 0)
        sizer_slave.Add(sizer_x, 0, wx.RIGHT|wx.EXPAND, 0)
        sizer_2.Add(sizer_slave, 0, wx.EXPAND, 0)

        sizer_block = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Block"), wx.VERTICAL)
        sizer_x = wx.BoxSizer(wx.HORIZONTAL)
        sizer_x.Add(self.label_2, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
        sizer_x.Add(self.text_ctrl_block_name, 1, 0, 0)
        sizer_block.Add(sizer_x, 0, wx.RIGHT|wx.EXPAND, 0)
        sizer_x = wx.BoxSizer(wx.HORIZONTAL)
        sizer_x.Add(self.label_3, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
        sizer_x.Add(self.combo_ctrl_block_type, 1, 0, 0)
        sizer_block.Add(sizer_x, 0, wx.RIGHT|wx.EXPAND, 0)
        sizer_x = wx.BoxSizer(wx.HORIZONTAL)
        sizer_x.Add(self.label_4, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
        sizer_x.Add(self.text_ctrl_start_addr, 1, 0, 0)
        sizer_block.Add(sizer_x, 0, wx.RIGHT|wx.EXPAND, 0)
        sizer_x = wx.BoxSizer(wx.HORIZONTAL)
        sizer_x.Add(self.label_5, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 4)
        sizer_x.Add(self.text_ctrl_addr_len, 1, 0, 0)
        sizer_block.Add(sizer_x, 0, wx.RIGHT|wx.EXPAND, 0)
        sizer_2.Add(sizer_block, 0, wx.EXPAND, 0)

        sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_3.Add(self.button_ok, 0, 0, 0)
        sizer_3.Add(self.button_cancel, 0, 0, 0)
        sizer_2.Add(sizer_3, 0, wx.ALL|wx.ALIGN_RIGHT, 4)
        self.SetAutoLayout(1)
        self.SetSizer(sizer_2)
        sizer_2.Fit(self)
        sizer_2.SetSizeHints(self)
        self.Layout()

    def __attach_events(self):
        wx.EVT_BUTTON(self, self.button_ok.GetId(), self.OnOK)
        wx.EVT_BUTTON(self, self.button_cancel.GetId(), self.OnCancel)

    def parse_int(self, label, text_ctrl):
        try:
            return int(text_ctrl.GetValue())
        except ValueError:
            dlg = wx.MessageDialog(self, label + ' must be a numeric value',
                                        'Value Error', wx.OK | wx.ICON_ERROR)
            dlg.ShowModal()
            dlg.Destroy()
        

    def OnOK(self, events):
        self.slave_addr = self.parse_int(self.label_1.GetLabelText(), self.text_ctrl_slave_addr)
        self.block_name = self.text_ctrl_block_name.GetValue()
        self.block_type = self.combo_ctrl_block_type.GetClientData(self.combo_ctrl_block_type.GetSelection())
        self.start_addr = self.parse_int(self.label_4.GetLabelText(), self.text_ctrl_start_addr)
        self.addr_len = self.parse_int(self.label_5.GetLabelText(), self.text_ctrl_addr_len)
        if self.slave_addr != None and self.start_addr != None and self.addr_len != None:
            self.EndModal(wx.ID_OK)

    def OnCancel(self, events):
        self.EndModal(wx.ID_CANCEL)

# end of class ModbusConfigDialog

#----------------------------------------------------------------------

class ChildPanel(wx.Panel):

    def __init__(self, parent, slave_block):
        wx.Panel.__init__(self, parent=parent)
        self.slave_block = slave_block
        self.val_ctrls = [None] * slave_block.addr_len
        gb = wx.GridBagSizer(3, 16)
        
        button = wx.Button(self, -1, "Get Values")
        gb.Add(button, (0,0))
        wx.EVT_BUTTON(self, button.GetId(), self.on_evt_button_get)
        button = wx.Button(self, -1, "Set Values")
        gb.Add(button, (0,1))
        wx.EVT_BUTTON(self, button.GetId(), self.on_evt_button_set)
        gb.Add(wx.StaticText(self, -1, "Address Offset", style=wx.ALIGN_CENTRE), (1, 0))
        gb.Add(wx.StaticText(self, -1, "Value", style=wx.ALIGN_CENTRE), (1, 1))
        for i in range(slave_block.addr_len):
            addr = slave_block.start_addr + i;
            row = i + 2
            s = wx.StaticText(self, -1, "0x%04x(%d)" % (addr, addr), style=wx.ALIGN_CENTRE)
            gb.Add(s, (row, 0))
            self.val_ctrls[i] = wx.TextCtrl(self, -1, "0")
            gb.Add(self.val_ctrls[i], (row, 1))
        self.SetSizer(gb)

    def on_evt_button_get(self, evt):
        try:
            self.get_values()
        except Exception as e:
            print 'on_evt_button', e

    def on_evt_button_set(self, evt):
        try:
            self.set_values()
        except Exception as e:
            print 'on_evt_button', e

    def set_values(self):
        values = []
        for ctrl in self.val_ctrls:
            values.append(int(ctrl.GetValue()))
        slave = modbus_server.get_slave(self.slave_block.slave_addr)
        slave.set_values(self.slave_block.block_name, self.slave_block.start_addr, values)
        print 'done: values written', values

    def get_values(self):
        slave = modbus_server.get_slave(self.slave_block.slave_addr)
        values = slave.get_values(self.slave_block.block_name, self.slave_block.start_addr, self.slave_block.addr_len)
        for i, v in enumerate(values):
            self.val_ctrls[i].SetValue(str(v))
        print 'done: values read:', values

#----------------------------------------------------------------------

class ChildFrame(wx.MDIChildFrame):
    
    def __init__(self, *args, **kwargs):
        self.slave_block = kwargs['slave_block']
        del kwargs['slave_block']
        wx.MDIChildFrame.__init__(self, *args, **kwargs)
        self.build_controls()
        
    def build_controls(self):
        self.pannel = ChildPanel(self, self.slave_block)
        self.SetClientSize(self.pannel.GetBestSize())

#----------------------------------------------------------------------

class MyParentFrame(wx.MDIParentFrame):
    def __init__(self):
        wx.MDIParentFrame.__init__(self, None, -1, "MODBUS Simulator", size=(800,600))

        self.serial = serial.Serial()
        
        menu = wx.Menu()
        menu.Append(ID_New, "&Add MODBUS block")
        menu.Enable(ID_New, False)
        menu.AppendSeparator()
        menu.Append(ID_PortSettings, "&Port Settings...")
        menu.Append(ID_Exit, "E&xit")

        menubar = wx.MenuBar()
        menubar.Append(menu, "&File")
        self.SetMenuBar(menubar)

        self.CreateStatusBar()

        self.Bind(wx.EVT_MENU, self.OnNewWindow, id=ID_New)
        self.Bind(wx.EVT_MENU, self.OnPortSettings, id=ID_PortSettings)
        self.Bind(wx.EVT_MENU, self.OnExit, id=ID_Exit)

        self.bg_bmp = get_bg_image()
        self.GetClientWindow().Bind(
            wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground
            )

        self.OnPortSettings(None)
        self.Bind(wx.EVT_CLOSE, self.on_evt_close)

    def OnExit(self, evt):
        self.Close(True)

    def on_evt_close(self, event):
        print 'on_evt_close'
        if modbus_server:
            modbus_server.stop()
            modbus_server.close()
        event.Skip()
    
    def OnNewWindow(self, evt):
        dlg = ModbusConfigDialog(self)
        if dlg.ShowModal() == wx.ID_OK:
            title = "%s: #0x%x:%d,%d" % (dlg.block_name, dlg.slave_addr, dlg.slave_addr, dlg.block_type)
            slave_block = SlaveBlock(dlg.slave_addr, dlg.block_name, dlg.block_type, dlg.start_addr, dlg.addr_len)
            try:
                self.add_slave_block(slave_block)
            except Exception, e:
                dlg = wx.MessageDialog(self, str(e), "Error while adding block", wx.OK | wx.ICON_ERROR)
                dlg.ShowModal()
                dlg.Destroy()
            else:
                win = ChildFrame(self, -1, title, slave_block=slave_block)
                win.Show(True)
            
    def add_slave_block(self, slave_block):
        try:
            modbus_server.add_slave(slave_block.slave_addr)
        except Exception as e:
            print 'add_slave_block:', e
        slave = modbus_server.get_slave(slave_block.slave_addr)
        slave.add_block(slave_block.block_name, slave_block.block_type, slave_block.start_addr, slave_block.addr_len)
        print 'done: block %s added' % (slave_block.block_name)

    def OnPortSettings(self, event=None):
        dlg_cfg = wxSerialConfigDialog.SerialConfigDialog(None, -1, "",
            show=wxSerialConfigDialog.SHOW_BAUDRATE,
            serial=self.serial
        )
        result = dlg_cfg.ShowModal()
        dlg_cfg.Destroy()
        if result == wx.ID_OK:
            try:
                self.serial.open()
            except serial.SerialException, e:
                dlg = wx.MessageDialog(None, str(e), "Serial Port Error", wx.OK | wx.ICON_ERROR)
                dlg.ShowModal()
                dlg.Destroy()
            else:
                global modbus_server
                print 'Port open ok', self.serial.portstr
                modbus_server = modbus_rtu.RtuServer(self.serial)
                modbus_server.start()
                menu = self.GetMenuBar().GetMenu(0)
                menu.Enable(ID_New, True)
                menu.Enable(ID_PortSettings, False)

    def OnEraseBackground(self, evt):
        dc = evt.GetDC()

        # tile the background bitmap
        sz = self.GetClientSize()
        w = self.bg_bmp.GetWidth()
        h = self.bg_bmp.GetHeight()
        x = 0
        
        while x < sz.width:
            y = 0

            while y < sz.height:
                dc.DrawBitmap(self.bg_bmp, x, y)
                y = y + h

            x = x + w

#----------------------------------------------------------------------

if __name__ == '__main__':
    class MyApp(wx.App):
        def OnInit(self):
            frame = MyParentFrame()
            frame.Show(True)
            self.SetTopWindow(frame)
            return True

    logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s")
    MyApp(False).MainLoop()

